﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using FStore.Utils;

namespace FStore
{
    /// <summary>
    /// Class to help reduce the usage of side-effect less classes, or unnecessary creation of multiple objects that could
    /// be used interchangeably.
    /// </summary>
    public static class GCache
    {
        // Timer to kick off and keep the weak cache in check
        private static Timer _weakSweepTimer = null;

        static GCache()
        {
            // Reserve the random domain if need be
            if (!Unique.IsDomainReservedRandomInt32(consts.GCache_Domain))
            {
                Unique.ReserveInt32RandomDomain(consts.GCache_Domain);
            }

            // Hook up the sweep timer
            // Run it the first time in 60 seconds, then every 30 seconds
            _weakSweepTimer = new Timer((cb) => doWeakSweep(), null, TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(30));
        }

        // Holds the weakly cached objects
        private static Dictionary<string, WeakReference> weakCache = new Dictionary<string, WeakReference>();

        // Holds the strongly cached objects
        private static Dictionary<string, object> strongCache = new Dictionary<string, object>();

        /// <summary>
        /// Retrieves an object from the weak cache
        /// </summary>
        /// <param name="type">The object type</param>
        /// <param name="strRef">The unique reference for the object</param>
        /// <param name="bConstruct">Whether to create an instance if one wasn't found</param>
        /// <returns>
        /// The cached object or the newly constructed object if <paramref name="bConstruct"/> is <c>True</c>, 
        /// or null if <paramref name="bConstruct"/> was <c>False</c> and an object
        /// with a reference(<paramref name="strRef"/>) wasn't found; if the cached item was garbage collected
        /// and <paramref name="bConstruct"/> was <c>True</c> then a new item will be constructed.
        /// </returns>
        internal static object GetWeakItem(Type type, string strRef, bool bConstruct = false)
        {
            lock (weakCache)
            {
                // Build a reference
                strRef = type.AssemblyQualifiedName + "^" + strRef;

                // Reference not found
                if (!weakCache.ContainsKey(strRef))
                {
                    // Not allowed to construct
                    if (bConstruct == false)
                    {
                        return null;
                    }
                    else
                    {
                        // Create and add the object to the cache
                        object created = Activator.CreateInstance(type);
                        weakCache.Add(strRef, new WeakReference(created));

                        // Return the object
                        return weakCache[strRef].Target;
                    }
                }

                // Get the weakly-referenced cache entry
                WeakReference wRef = weakCache[strRef];

                // Cache item was collected
                if (wRef.IsAlive == false)
                {
                    // Are we allowed to re-construct the item?
                    if (bConstruct)
                    {
                        // Construct the item
                        weakCache[strRef].Target = Activator.CreateInstance(type);

                        // Return it
                        return weakCache[strRef].Target;
                    }
                    else
                    {
                        // Inform caller the get failed
                        return null;
                    }
                }
                else
                {
                    // Check the cache item is of the correct type
                    if (type.IsInstanceOfType(wRef.Target))
                    {
                        // Return the item
                        return wRef.Target;
                    }
                    else
                    {
                        // Exception
                        throw new Exception("Cache entry with ref [{0}] is of [{1}} expecting [{2}]".UFormat(
                            strRef, wRef.Target.GetType().FullName, type.FullName));
                    }
                }
            }
        }

        /// <summary>
        /// Retrieves an object from the weak cache
        /// </summary>
        /// <typeparam name="T">The object type</typeparam>
        /// <param name="strRef">The unique reference for the object</param>
        /// <param name="bConstruct">Whether to create an instance if one wasn't found</param>
        /// <returns>
        /// The cached object or the newly constructed object if <paramref name="bConstruct"/> is <c>True</c>, 
        /// or default(<typeparamref name="T"/>) if <paramref name="bConstruct"/> was <c>False</c> and an object
        /// with a reference(<paramref name="strRef"/>) wasn't found; if the cached item was garbage collected
        /// and <paramref name="bConstruct"/> was <c>True</c> then a new item will be constructed.
        /// </returns>
        public static T GetWeakItem<T>(string strRef, bool bConstruct = false)
        {
            // Get the weak item
            return (T)GetWeakItem(typeof(T), strRef, bConstruct);
        }

        /// <summary>
        /// Perform a sweep of the weak cache and remove them if they're dead
        /// and free up any random identifiers that were used
        /// </summary>
        private static void doWeakSweep()
        {
            List<string> forRemoval = new List<string>();

            lock (weakCache)
            {
                // Go through each weakly cached item
                foreach (var item in weakCache)
                {
                    // Got a dead one
                    if (item.Value.IsAlive == false)
                    {
                        // It could be an item with a uniquely reserved number used as part of the key
                        if (item.Key.Contains("~"))
                        {
                            string[] pieces = item.Key.Split('~');

                            // Check it's the right format used for it
                            if (pieces.Length == 2)
                            {
                                int iNum = int.MinValue;

                                // We have a valid number
                                if (int.TryParse(pieces[1], out iNum))
                                {
                                    // And it's one of the reserved numbers
                                    if (Unique.IsRandomInt32RegisteredForDomain(iNum, consts.GCache_Domain))
                                    {
                                        // Remove it so it can be used again
                                        Unique.RemoveRandomInt32(iNum, consts.GCache_Domain);
                                    }
                                }
                            }
                        }

                        // Add this item for removal
                        forRemoval.Add(item.Key);
                    }
                }

                // Now remove any cache entries that were dead
                foreach (string reference in forRemoval)
                {
                    weakCache.Remove(reference);
                }
            }
        }

        /// <summary>
        /// Allows a weakly cached item to be returned for when you don't care about the reference
        /// or you want to perform your own selection logic
        /// </summary>
        /// <typeparam name="T">The type of the item to get</typeparam>
        /// <param name="selector">
        /// Allows you to perform selection on the cached items found, defaults to null to specify that the first
        /// item found alive should be used
        /// </param>
        /// <param name="bConstruct">
        /// Specifies whether it is ok to construct the object if no suitable entries were found
        /// OR if the item chosen by selection ended up being collected, this means when set to <c>True</c>
        /// the selector isn't guaranteed.
        /// </param>
        /// <returns>
        /// The cached item if one was found OR the constructed item if <paramref name="bConstruct"/> was <c>True</c>
        /// OR default(<typeparamref name="T"/>) if no alive items of <typeparamref name="T"/> were found initially
        /// </returns>
        public static T GetWeakItemByType<T>(Func<List<KeyValuePair<string, T>>, string> selector = null,
            bool bConstruct = true)
        {
            // Create the new wrapper selector
            Func<List<KeyValuePair<string, object>>, string> nonGenericSelector = new Func<List<KeyValuePair<string, object>>, string>(
                (items) =>
                {
                    List<KeyValuePair<string, T>> nItems = new List<KeyValuePair<string, T>>();
                    foreach (var item in items)
                    {
                        nItems.Add(new KeyValuePair<string, T>(item.Key, (T)item.Value));
                    }

                    return selector(nItems);
                });
            
            // Return the value with the new selector
            return (T)GetWeakItemByType(typeof(T), nonGenericSelector, bConstruct);
        }

        /// <summary>
        /// Allows a weakly cached item to be returned for when you don't care about the reference
        /// or you want to perform your own selection logic
        /// </summary>
        /// <param name="type">The type of the item to get</param>
        /// <param name="selector">
        /// Allows you to perform selection on the cached items found, defaults to null to specify that the first
        /// item found alive should be used
        /// </param>
        /// <param name="bConstruct">
        /// Specifies whether it is ok to construct the object if no suitable entries were found
        /// OR if the item chosen by selection ended up being collected, this means when set to <c>True</c>
        /// the selector isn't guaranteed.
        /// </param>
        /// <returns>
        /// The cached item if one was found OR the constructed item if <paramref name="bConstruct"/> was <c>True</c>
        /// OR null if no alive items of <paramref name="type"/> were found initially
        /// </returns>
        internal static object GetWeakItemByType(Type type, Func<List<KeyValuePair<string, object>>, string> selector = null,
            bool bConstruct = true)
        {
            // Single anonymous function to do the unique ID construction
            Func<object> doConstruct = () =>
            {
                return GetWeakItem(type, "~" + Unique.GetRandomInt32(consts.GCache_Domain).ToString(),
                    true);
            };

            lock (weakCache)
            {
                // Get the currently alive items that are of type T
                var aliveItems =
                    weakCache.Where(wr => wr.Value.IsAlive).
                    Where(wr => type.IsInstanceOfType(wr.Value.Target));

                int iCount = 0;

                foreach (var item in aliveItems)
                {
                    // Item may have been collected between start and now
                    // skip it if it was
                    if (item.Value.IsAlive == false)
                        continue;

                    iCount++;
                }

                if (iCount == 0)    // No items were found
                {
                    // Allowed to construct
                    if (bConstruct == true)
                    {
                        //return GetWeakItem<T>(Unique.GetRandomInt32(consts.GCache_Domain).ToString(), true);
                        //return GetWeakItem(type, Unique.GetRandomInt32(consts.GCache_Domain).ToString(), true);
                        return doConstruct();
                    }
                    else
                    {
                        // Not allowed to construct
                        return null;
                    }
                }
                else
                {
                    // Items were found, but a selector wasn't defined
                    if (selector == null)
                    {
                        // Return the first alive cached item
                        foreach (var item in aliveItems)
                        {
                            // Check that the item is alive, because it could have been collected
                            if (item.Value.IsAlive)
                            {
                                return item.Value.Target;
                            }
                        }

                        // All the previously alive items have expired if we reach this point

                        // Check if we can construct an object
                        if (bConstruct == true)
                        {
                            return doConstruct();
                        }
                        else
                        {
                            throw new Exception(
                                "Failed to get weak item by type [{0}], as construction was disabled and " +
                                "the chosen object expired before it could be returned".UFormat(type.FullName));
                        }
                    }
                    // Items were found AND a selector was defined
                    else
                    {
                        List<KeyValuePair<string, object>> objectsForSelector = new List<KeyValuePair<string, object>>();

                        // Go through all the previously known alive items
                        foreach (var item in aliveItems)
                        {
                            // Skip this item if it isn't alive
                            if (item.Value.IsAlive == false)
                                continue;

                            objectsForSelector.Add(new KeyValuePair<string, object>(
                                item.Key, item.Value.Target));
                        }

                        if (objectsForSelector.Count > 0)
                        {
                            string chosenRef = selector(objectsForSelector);

                            // The selector returned an empty reference
                            if (string.IsNullOrEmpty(chosenRef))
                            {
                                throw new Exception(
                                    "Failed to get weak item by type [{0}], reference returned by selector cannot be null or empty".UFormat(type.FullName));
                            }

                            // The reference returned by the selector isn't one of the items
                            if (objectsForSelector.Any(item => item.Key == chosenRef) == false)
                            {
                                throw new Exception(
                                    "Failed to get weak item by type [{0}], reference returned by selector wasn't one of those supplied".UFormat(type.FullName));
                            }

                            // Check if the object expired while selection was taking place
                            if (weakCache[chosenRef].IsAlive)
                            {
                                return weakCache[chosenRef].Target;
                            }
                            else
                            {
                                // It expired but we're allowed to construct
                                if (bConstruct)
                                {
                                    return doConstruct();
                                }
                                else
                                {
                                    throw new Exception(
                                    "Failed to get weak item by type [{0}], as construction was disabled and " +
                                    "the object chosen by selection expired before it could be returned".UFormat(type.FullName));
                                }
                            }
                        }
                        else // No alive objects remain
                        {
                            // No matter, we can just construct one
                            if (bConstruct == true)
                            {
                                return doConstruct();
                            }
                            else
                            {
                                throw new Exception(
                                    "Failed to get weak item by type [{0}], as construction was disabled and " +
                                    "the alive objects expired before they could be passed for selection".UFormat(type.FullName));
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Retrieves an object from the strong cache
        /// </summary>
        /// <typeparam name="T">The object type</typeparam>
        /// <param name="strRef">The unique reference for the object</param>
        /// <param name="bConstruct">Whether to create an instance if one wasn't found</param>
        /// <returns>
        /// The cached object or the newly constructed object if <paramref name="bConstruct"/> is <c>True</c>, 
        /// or default(<typeparamref name="T"/>) if <paramref name="bConstruct"/> was <c>False</c> and an object
        /// with a reference(<paramref name="strRef"/>) wasn't found
        /// </returns>
        public static T GetStrongItem<T>(string strRef, bool bConstruct = false)
        {
            lock (strongCache)
            {
                // Build a reference
                strRef = typeof(T).AssemblyQualifiedName + "^" + strRef;

                // Reference not found
                if (!strongCache.ContainsKey(strRef))
                {
                    // Not allowed to construct
                    if (bConstruct == false)
                    {
                        return default(T);
                    }
                    else
                    {
                        // Create and add the object to the cache
                        T created = Activator.CreateInstance<T>();
                        strongCache.Add(strRef, created);

                        // Return the object
                        return (T)strongCache[strRef];
                    }
                }

                // Get the cached object
                object objInCache = strongCache[strRef];

                // Check the cached object is of the correct type
                if (objInCache is T)
                {
                    // Return the cached item
                    return (T)objInCache;
                }
                else
                {
                    // Exception
                    throw new Exception("Cache entry with ref [{0}] is of [{1}} expecting [{2}]".UFormat(
                        strRef, objInCache.GetType().FullName, typeof(T).FullName));
                }
            }
        }
    }
}
